import datetime
import random
import time
import traceback

from absl import app, flags

from coin.strategy.kr_hard_arb.arb_info import create_arb_info
from coin.strategy.kr_hard_arb.util import *

FLAGS = flags.FLAGS

flags.DEFINE_string('currency', '', '')

flags.DEFINE_string('exchange1', 'Exchange1', '')

flags.DEFINE_string('exchange2', 'Exchange2', '')

flags.DEFINE_string('order_subscriber_api_version1', None, 'API version of exchange1.')

flags.DEFINE_string('order_subscriber_api_version2', None, 'API version of exchange2.')

flags.DEFINE_string('key_file1', '', '')

flags.DEFINE_string('key_file2', '', '')

flags.DEFINE_string('config_filename', '', '')

flags.DEFINE_bool('dry_run', False, 'dry_run')

flags.DEFINE_bool('ignore_reserve_offset', True, 'ignore_reserve_offset')


def get_ref_price(info, currency):
  price_a = info.exch1.feed.get_best_price()
  price_b = info.exch2.feed.get_best_price()
  if price_a is None:
    raise ValueError("info.exch1.feed.get_best_price()", info.exch1.feed.get_best_price())

  if price_b is None:
    raise ValueError("info.exch2.feed.get_best_price()", info.exch2.feed.get_best_price())

  ref_price = (price_a.mid_price + price_b.mid_price) * 0.5

  if (abs(price_a.mid_price - price_b.mid_price) / ref_price) > 0.2:
    raise ValueError("price diff is serious", str(price_a), str(price_b))

  return ref_price


def has_enough_bal(bal, threshold):
  return bal.available >= threshold * 1.01


def buy_exch1_sell_exch2(exch2_order,
                         exch1_order,
                         buy_price,
                         buy_qty,
                         sell_price,
                         sell_qty,
                         currency,
                         qty_threshold):
  if FLAGS.dry_run:
    return
  jobs = []
  if buy_qty > qty_threshold:
    jobs.append(
        ThreadRunner(exch1_order.query_submit,
                     exch1_order.get_product(currency),
                     "bid",
                     buy_price,
                     buy_qty))
  if sell_qty > qty_threshold:
    jobs.append(
        ThreadRunner(exch2_order.query_submit,
                     exch2_order.get_product(currency),
                     "ask",
                     sell_price,
                     sell_qty))
  try:
    execute_thread_runners(jobs, "Buy exch1 / Sell exch2")
    sleep_seconds(2)
  except Exception:
    traceback.print_exc()
  execute_thread_runners([
      ThreadRunner(exch1_order.query_cancel_product, exch1_order.get_product(currency)),
      ThreadRunner(exch2_order.query_cancel_product, exch2_order.get_product(currency))
  ],
                         "Cancel All")


def buy_exch2_sell_exch1(exch2_order,
                         exch1_order,
                         buy_price,
                         buy_qty,
                         sell_price,
                         sell_qty,
                         currency,
                         qty_threshold):
  if FLAGS.dry_run:
    return
  jobs = []
  if buy_qty > qty_threshold:
    jobs.append(
        ThreadRunner(exch2_order.query_submit,
                     exch2_order.get_product(currency),
                     "bid",
                     buy_price,
                     buy_qty))
  if sell_qty > qty_threshold:
    jobs.append(
        ThreadRunner(exch1_order.query_submit,
                     exch1_order.get_product(currency),
                     "ask",
                     sell_price,
                     sell_qty))
  try:
    execute_thread_runners(jobs, "Buy exch2 / Sell exch1")
    sleep_seconds(2)
  except Exception:
    traceback.print_exc()
  execute_thread_runners([
      ThreadRunner(exch1_order.query_cancel_product, exch1_order.get_product(currency)),
      ThreadRunner(exch2_order.query_cancel_product, exch2_order.get_product(currency))
  ],
                         "Cancel All")


def truncate(value):
  return int(value * 1000) / 1000.0


def get_currency_balance(order, currency):
  balance = order.get_balance_cached()
  if balance is None or balance.msg is None:
    raise RuntimeError("balance.msg is None")

  for each_balance in balance.msg.each_balance:
    if each_balance.currency == currency or (each_balance.currency == "BCC"
                                             and currency.lower() == "bch"):
      return each_balance

  from coin.exchange.upbit_v1.kr_rest.order_client import UpbitV1Order

  if isinstance(order, UpbitV1Order):
    from coin.proto.coin_query_pb2 import CurrencyBalance
    currency_balance = CurrencyBalance()
    currency_balance.currency = currency
    currency_balance.currency_native = currency
    currency_balance.available = 0.0
    currency_balance.hold = 0.0
    currency_balance.total = 0.0
    return currency_balance
  else:
    raise ValueError(currency, balance.msg)
  return None


def print_status(info, currency):
  ccy_price = get_ref_price(info, currency)

  exch2_krw = get_currency_balance(info.exch2.order, 'KRW')
  exch1_krw = get_currency_balance(info.exch1.order, 'KRW')
  exch2_ccy = get_currency_balance(info.exch2.order, currency)
  exch1_ccy = get_currency_balance(info.exch1.order, currency)

  total_value = (exch2_krw.total + exch2_ccy.total * ccy_price + exch1_krw.total
                 + exch1_ccy.total * ccy_price)
  krw_amount = total_value - \
               info.config["qty"] * ccy_price
  total_ccy = exch2_ccy.total + exch1_ccy.total
  print("Estimated Value: %s" % ("{:,.0f}".format(total_value)))
  print("Krw Value: %s" % ("{:,.0f}".format(krw_amount)))
  print("%s Value: %s * %s = %s" %
        (currency, ccy_price, info.config["qty"], ccy_price * info.config["qty"]))
  print("%s \t Total: %8.4f \t %s: %8.4f \t %s: %8.4f" % (currency,
                                                          total_ccy,
                                                          info.exch2.exchange,
                                                          exch2_ccy.total,
                                                          info.exch1.exchange,
                                                          exch1_ccy.total))
  print("KRW \t\t\t\t %s: %s \t %s: %s" % (info.exch2.exchange,
                                           "{:,.0f}".format(exch2_krw.available),
                                           info.exch1.exchange,
                                           "{:,.0f}".format(exch1_krw.available)))


def get_buy_sell_qty(info, currency):
  lot_size_krw = info.config["lot_size_krw"]
  exch2_bal = get_currency_balance(info.exch2.order, currency)
  exch1_bal = get_currency_balance(info.exch1.order, currency)
  ref_price = get_ref_price(info, currency)

  total_coin = exch2_bal.total + exch1_bal.total
  lot_size = (lot_size_krw / float(ref_price)) * (1 + random.random() * 0.2)
  base_qty = max(lot_size - abs(info.config["qty"] - total_coin), 0)
  buy_qty = truncate(base_qty + max(info.config["qty"] - total_coin, 0))
  sell_qty = truncate(base_qty + max(total_coin - info.config["qty"], 0))
  return (buy_qty, sell_qty)


def is_expected_deposit(info, currency):
  exch2_bal = get_currency_balance(info.exch2.order, currency)
  exch1_bal = get_currency_balance(info.exch1.order, currency)
  total_coin = exch2_bal.total + exch1_bal.total
  expected_total_coin = info.config["qty"]
  qty_offset_ratio = info.config.get("qty_offset_ratio", 0.3)
  diff = abs(expected_total_coin - total_coin)
  print("Diff: %.2f, Threshold: %.2f" % (diff, expected_total_coin * qty_offset_ratio))
  if FLAGS.ignore_reserve_offset:
    return True
  if diff > expected_total_coin * qty_offset_ratio:
    return False
  else:
    return True


die_flag = False


def trade_coin(info, currency):
  if not is_expected_deposit(info, currency):
    print()
    print(("It seems that %s deposit is quite different "
           "from expectation. Skip trading...") % currency)
    return

  if (hasattr(info.exch1.feed, "exchange_time") and info.exch1.feed.exchange_time):
    ref_time1 = info.exch1.feed.exchange_time
  else:
    ref_time1 = info.exch1.feed.fetched_time

  if (hasattr(info.exch2.feed, "exchange_time") and info.exch2.feed.exchange_time):
    ref_time2 = info.exch2.feed.exchange_time
  else:
    ref_time2 = info.exch2.feed.fetched_time

  diff_time = abs(ref_time1 - ref_time2)
  if (diff_time > 10 * 1e+9):
    print("tdiff=%.3f, %s time1 = %s, %s time2 = %s" %
          (diff_time / 1e9, info.exch1.exchange, ref_time1, info.exch2.exchange, ref_time2))
    if diff_time > 30 * 1e+9:
      die_flag = True
      raise ValueError("diff is serious")
    return
  else:
    print("tdiff=%.3f, normal" % (diff_time / 1e9))

  price_det_notional = info.config["price_det_notional"]
  rate_threshold = info.config["rate_threshold"]
  urgent_rate_threshold = info.config["urgent_rate_threshold"]
  qty_threshold = info.config["min_qty"]
  exch1_price = info.exch1.feed.get_price_by_notional(price_det_notional)
  exch2_price = info.exch2.feed.get_price_by_notional(price_det_notional)

  print("%s: %s" % (info.exch1.exchange, exch1_price))
  print("%s: %s" % (info.exch2.exchange, exch2_price))

  if exch1_price is None or exch2_price is None:
    return

  exch2_krw = get_currency_balance(info.exch2.order, 'KRW')
  exch1_krw = get_currency_balance(info.exch1.order, 'KRW')
  exch2_bal = get_currency_balance(info.exch2.order, currency)
  exch1_bal = get_currency_balance(info.exch1.order, currency)

  buy_qty, sell_qty = get_buy_sell_qty(info, currency)

  to_exch2 = float(exch2_price.bid_price) / float(exch1_price.ask_price) - 1
  urgency = exch1_bal.total < (info.config["qty"] * 0.2)
  if to_exch2 > rate_threshold or (to_exch2 > urgent_rate_threshold and urgency):

    buy_qty_max = exch1_krw.available / exch1_price.ask_price * 0.99
    sell_qty_max = exch2_bal.available * 0.99
    buy_adj_qty = max(buy_qty - buy_qty_max, 0)
    sell_adj_qty = max(sell_qty - sell_qty_max, 0)
    adj_qty = max(buy_adj_qty, sell_adj_qty)
    buy_qty = truncate(buy_qty - adj_qty)
    sell_qty = truncate(sell_qty - adj_qty)

    print("Buy %s / Sell %s" % (info.exch1.exchange, info.exch2.exchange))
    print("Urgency:", urgency)
    print("Rate: %.4f%%" % (to_exch2 * 100))
    print("Buy  %.4f at %.2f" % (buy_qty, exch1_price.ask_price))
    print("Sell %.4f at %.2f" % (sell_qty, exch2_price.bid_price))

    has_quote = has_enough_bal(exch1_krw, buy_qty * exch1_price.ask_price)
    has_coin = has_enough_bal(exch2_bal, sell_qty)
    minqty_coin = (buy_qty > qty_threshold or sell_qty > qty_threshold)
    do = has_quote and has_coin and minqty_coin
    if do:
      try:
        buy_exch1_sell_exch2(info.exch2.order,
                             info.exch1.order,
                             exch1_price.ask_price,
                             buy_qty,
                             exch2_price.bid_price,
                             sell_qty,
                             currency,
                             qty_threshold)
      except Exception:
        traceback.print_exc()
    else:
      print("Cannot trade:", has_quote, has_coin, minqty_coin)

  to_exch1 = float(exch1_price.bid_price) / float(exch2_price.ask_price) - 1
  urgency = exch2_bal.total < (info.config["qty"] * 0.2)
  if to_exch1 > rate_threshold or (to_exch1 > urgent_rate_threshold and urgency):

    buy_qty_max = exch2_krw.available / exch2_price.ask_price * 0.99
    sell_qty_max = exch1_bal.available * 0.99
    buy_adj_qty = max(buy_qty - buy_qty_max, 0)
    sell_adj_qty = max(sell_qty - sell_qty_max, 0)
    adj_qty = max(buy_adj_qty, sell_adj_qty)
    buy_qty = truncate(buy_qty - adj_qty)
    sell_qty = truncate(sell_qty - adj_qty)

    print("Buy %s / Sell %s" % (info.exch2.exchange, info.exch1.exchange))
    print("Urgency:", urgency)
    print("Rate: %.4f%%" % (to_exch1 * 100))
    print("Buy  %.4f at %.2f" % (buy_qty, exch2_price.ask_price))
    print("Sell %.4f at %.2f" % (sell_qty, exch1_price.bid_price))

    has_quote = has_enough_bal(exch2_krw, buy_qty * exch2_price.ask_price)
    has_coin = has_enough_bal(exch1_bal, sell_qty)
    minqty_coin = (buy_qty > qty_threshold or sell_qty > qty_threshold)
    do = has_quote and has_coin and minqty_coin
    if do:
      try:
        buy_exch2_sell_exch1(info.exch2.order,
                             info.exch1.order,
                             exch2_price.ask_price,
                             buy_qty,
                             exch1_price.bid_price,
                             sell_qty,
                             currency,
                             qty_threshold)
      except Exception:
        traceback.print_exc()
    else:
      print("Cannot trade:", has_quote, has_coin, minqty_coin)


def trade(info, currency):
  try:
    print()
    print_status(info, currency)
    trade_coin(info, currency)
  except KeyboardInterrupt:
    raise
  except Exception:
    print("A problem detected. Try again")
    traceback.print_exc()


def main(argv):
  currency = FLAGS.currency
  info = create_arb_info(currency,
                         FLAGS.exchange1,
                         FLAGS.exchange2,
                         FLAGS.key_file1,
                         FLAGS.key_file2,
                         dict(order_subscriber=FLAGS.order_subscriber_api_version1),
                         dict(order_subscriber=FLAGS.order_subscriber_api_version2),
                         FLAGS.config_filename)

  while (True):
    time.sleep(0.2)
    print()
    print("Time:", datetime.datetime.now())
    print("Checking...")
    trade(info, currency)
    if die_flag:
      break
  return 0


if __name__ == '__main__':
  app.run(main)
